from typing import (
    Dict,
    List,
    Optional,
    Tuple,
    Union,
)
"""
Directory Service - Manages run directories and file path operations.

Primary Responsibility: All run directory and file path management

Focus Areas:
- Run directories: Creation, discovery, and organization of pipeline run directories
- Organization directories: Structure validation and path resolution for org directories
- Stage directories: Output directory creation and management for pipeline stages
- Provider/month organization: File organization patterns for scalable data storage
- File path construction: Consistent path building and validation across the system
- Directory validation: Structure verification and error handling for directory operations

Does NOT handle: Data loading/saving, configuration parsing, business logic

This service preserves all existing directory management behavior including:
- Exact timestamp formats for run directory naming
- Provider/month structure for memory-efficient processing
- Log directory mirroring for debugging and monitoring
- Sequential stage dependency validation
- Workspace structure validation for uploads
- Atomic directory operations and error handling
"""

import re
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path


@dataclass
class DirectoryPaths:
    """
    Container for standard directory paths within an organization.

    Centralizes path construction and provides clear documentation
    of the expected directory structure for organizations.
    """

    org_dir: Path
    billing_dir: Path = field(init=False)
    runs_dir: Path = field(init=False)
    logs_dir: Path = field(init=False)
    telemetry_dir: Path = field(init=False)
    config_file: Path = field(init=False)

    def __post_init__(self):
        """Initialize derived paths based on org_dir."""
        self.billing_dir = self.org_dir / "billing"
        self.runs_dir = self.billing_dir / "runs"
        self.logs_dir = self.org_dir.parent.parent / "logs" / self.org_dir.name
        self.telemetry_dir = self.org_dir / "telemetry"
        self.config_file = self.org_dir / "config.yaml"


@dataclass
class RunDirectoryInfo:
    """
    Information about a pipeline run directory.

    Contains metadata about run directories including timing,
    stage completion status, and directory structure.
    """

    run_id: str
    run_dir: Path
    org_name: str
    created_timestamp: datetime
    stages_completed: List[str] = field(default_factory=list)
    is_current: bool = False

    @property
    def run_timestamp_str(self) -> str:
        """Get run timestamp in standard format."""
        return self.created_timestamp.strftime("%Y-%m-%d_%H%M%S")


@dataclass
class StageDirectoryInfo:
    """
    Information about a stage output directory.

    Contains metadata about stage directories including structure type,
    provider/month organization, and file counts.
    """

    stage_name: str
    stage_dir: Path
    run_dir: Path
    structure_type: str  # "flat", "provider_partitioned", "provider_month_partitioned"
    provider_months: List[Tuple[str, str]] = field(default_factory=list)
    total_files: int = 0


class DirectoryService:
    """
    Service for all directory management and file path operations.

    Provides centralized directory management while preserving all existing
    directory creation, discovery, and organization patterns used throughout
    the cz-billing-generator system.

    Key Features:
    - Run directory creation with timestamp-based naming
    - Organization directory structure validation and discovery
    - Provider/month directory organization for scalable data processing
    - Stage directory management with different partitioning strategies
    - Log directory mirroring for debugging and monitoring
    - Atomic directory operations with proper error handling
    """

    def __init__(self, base_dir: Optional[Union[str, Path]] = None):
        """
        Initialize DirectoryService.

        Args:
            base_dir: Base directory for operations (defaults to current working directory)
        """
        self.base_dir = Path(base_dir) if base_dir else Path.cwd()
        self._ensure_base_directory_structure()

    # === Run Directory Management ===

    def create_run_directory(
        self, org_name: str, run_id: Optional[str] = None, stage_name: str = "generate"
    ) -> RunDirectoryInfo:
        """
        Create new pipeline run directory.

        Creates timestamped run directory following the standard pattern:
        {org_dir}/billing/runs/{YYYY-MM-DD_HHMMSS}/

        Preserves existing behavior including:
        - Generate stage always creates new directory
        - Other stages can reuse existing directory
        - Timestamp format matching existing conventions
        - Log directory mirroring

        Args:
            org_name: Organization name
            run_id: Optional specific run ID (if None, generates timestamp)
            stage_name: Stage name for run creation rules

        Returns:
            RunDirectoryInfo with created directory information

        Raises:
            FileNotFoundError: If org directory doesn't exist
            PermissionError: If cannot create directory
        """
        org_dir = self.get_org_directory(org_name)
        paths = DirectoryPaths(org_dir)

        # Determine run ID based on stage and existing directories
        if run_id is None:
            if stage_name == "generate":
                # Generate stage always creates new timestamped directory
                run_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")
            else:
                # Other stages try to use most recent run
                latest_run = self.find_latest_run_directory(org_name)
                if latest_run:
                    return latest_run
                else:
                    # No existing run, create new one
                    run_id = datetime.now().strftime("%Y-%m-%d_%H%M%S")

        # Create run directory
        run_dir = paths.runs_dir / run_id
        run_dir.mkdir(parents=True, exist_ok=True)

        # Create corresponding log directory (preserves existing mirroring behavior)
        log_run_dir = paths.logs_dir / "runs" / run_id
        log_run_dir.mkdir(parents=True, exist_ok=True)

        # Parse timestamp from run_id
        timestamp = self._parse_run_timestamp(run_id)

        return RunDirectoryInfo(
            run_id=run_id,
            run_dir=run_dir,
            org_name=org_name,
            created_timestamp=timestamp,
            is_current=True,
        )

    def find_existing_run_directory(
        self, org_name: str, run_id: str
    ) -> Optional[RunDirectoryInfo]:
        """
        Find existing run directory by ID.

        Args:
            org_name: Organization name
            run_id: Specific run ID to find

        Returns:
            RunDirectoryInfo if found, None otherwise
        """
        org_dir = self.get_org_directory(org_name)
        paths = DirectoryPaths(org_dir)

        run_dir = paths.runs_dir / run_id
        if run_dir.exists() and run_dir.is_dir():
            timestamp = self._parse_run_timestamp(run_id)
            stages = self._detect_completed_stages(run_dir)

            return RunDirectoryInfo(
                run_id=run_id,
                run_dir=run_dir,
                org_name=org_name,
                created_timestamp=timestamp,
                stages_completed=stages,
            )

        return None

    def find_latest_run_directory(self, org_name: str) -> Optional[RunDirectoryInfo]:
        """
        Find the most recent run directory.

        Preserves existing behavior of finding latest run by timestamp.

        Args:
            org_name: Organization name

        Returns:
            RunDirectoryInfo for latest run, None if no runs exist
        """
        org_dir = self.get_org_directory(org_name)
        paths = DirectoryPaths(org_dir)

        if not paths.runs_dir.exists():
            return None

        # Find all run directories with valid timestamp format
        run_dirs = []
        for run_dir in paths.runs_dir.iterdir():
            if run_dir.is_dir() and self._is_valid_run_timestamp(run_dir.name):
                timestamp = self._parse_run_timestamp(run_dir.name)
                stages = self._detect_completed_stages(run_dir)

                run_dirs.append(
                    RunDirectoryInfo(
                        run_id=run_dir.name,
                        run_dir=run_dir,
                        org_name=org_name,
                        created_timestamp=timestamp,
                        stages_completed=stages,
                    )
                )

        if not run_dirs:
            return None

        # Return most recent run
        latest_run = max(run_dirs, key=lambda r: r.created_timestamp)
        latest_run.is_current = True
        return latest_run

    def list_all_run_directories(self, org_name: str) -> List[RunDirectoryInfo]:
        """
        List all run directories for an organization.

        Args:
            org_name: Organization name

        Returns:
            List of RunDirectoryInfo sorted by timestamp (newest first)
        """
        org_dir = self.get_org_directory(org_name)
        paths = DirectoryPaths(org_dir)

        if not paths.runs_dir.exists():
            return []

        run_dirs = []
        for run_dir in paths.runs_dir.iterdir():
            if run_dir.is_dir() and self._is_valid_run_timestamp(run_dir.name):
                timestamp = self._parse_run_timestamp(run_dir.name)
                stages = self._detect_completed_stages(run_dir)

                run_dirs.append(
                    RunDirectoryInfo(
                        run_id=run_dir.name,
                        run_dir=run_dir,
                        org_name=org_name,
                        created_timestamp=timestamp,
                        stages_completed=stages,
                    )
                )

        # Sort by timestamp, newest first
        return sorted(run_dirs, key=lambda r: r.created_timestamp, reverse=True)

    # === Stage Directory Management ===

    def get_stage_directory(self, run_dir: Path, stage_name: str) -> Path:
        """
        Get stage output directory path.

        Creates consistent stage directory structure:
        {run_dir}/{stage_name}/

        Args:
            run_dir: Pipeline run directory
            stage_name: Stage name (e.g., "generate", "k8s", "metadata")

        Returns:
            Path to stage directory
        """
        stage_dir = run_dir / stage_name
        stage_dir.mkdir(parents=True, exist_ok=True)
        return stage_dir

    def create_provider_month_directory(
        self, stage_dir: Path, provider: str, month: str
    ) -> Path:
        """
        Create provider/month directory structure.

        Creates the standard provider/month organization:
        {stage_dir}/{provider}/{YYYY-MM}/

        Preserves existing pattern used for scalable data processing.

        Args:
            stage_dir: Stage output directory
            provider: Provider name (e.g., "AWS", "GCP", "Azure")
            month: Month in YYYY-MM format

        Returns:
            Path to provider/month directory

        Raises:
            ValueError: If month format is invalid
        """
        if not self._is_valid_month_format(month):
            raise ValueError(
                f"Invalid month format '{month}'. Expected YYYY-MM format."
            )

        provider_month_dir = stage_dir / provider / month
        provider_month_dir.mkdir(parents=True, exist_ok=True)
        return provider_month_dir

    def organize_provider_month_structure(
        self, stage_dir: Path
    ) -> Dict[Tuple[str, str], Path]:
        """
        Create complete provider/month directory structure.

        Sets up directory structure for all standard provider/month combinations
        commonly used in billing data processing.

        Args:
            stage_dir: Stage output directory

        Returns:
            Dictionary mapping (provider, month) tuples to directory paths
        """
        providers = ["AWS", "GCP", "Azure"]
        months = self._generate_month_range()  # Generate reasonable month range

        provider_month_dirs = {}
        for provider in providers:
            for month in months:
                provider_month_dir = self.create_provider_month_directory(
                    stage_dir, provider, month
                )
                provider_month_dirs[(provider, month)] = provider_month_dir

        return provider_month_dirs

    def discover_provider_months(self, stage_dir: Path) -> List[Tuple[str, str]]:
        """
        Discover existing provider/month combinations in stage directory.

        Scans stage directory for provider/month structure and returns all
        available combinations. Preserves existing discovery behavior.

        Args:
            stage_dir: Stage directory to scan

        Returns:
            List of (provider, month) tuples sorted alphabetically
        """
        if not stage_dir.exists():
            return []

        provider_months = []

        # Look for provider/month directory structure
        for provider_dir in stage_dir.iterdir():
            if provider_dir.is_dir():
                for month_dir in provider_dir.iterdir():
                    if month_dir.is_dir() and self._is_valid_month_format(
                        month_dir.name
                    ):
                        # Verify there's actually data in this month
                        data_file = month_dir / "data.parquet"
                        if data_file.exists():
                            provider_months.append((provider_dir.name, month_dir.name))

        # Sort for consistent processing order
        return sorted(provider_months)

    def get_stage_directory_info(
        self, run_dir: Path, stage_name: str
    ) -> StageDirectoryInfo:
        """
        Get comprehensive information about a stage directory.

        Analyzes stage directory structure and provides metadata
        about organization, files, and processing patterns.

        Args:
            run_dir: Pipeline run directory
            stage_name: Stage name

        Returns:
            StageDirectoryInfo with directory metadata
        """
        stage_dir = run_dir / stage_name

        if not stage_dir.exists():
            return StageDirectoryInfo(
                stage_name=stage_name,
                stage_dir=stage_dir,
                run_dir=run_dir,
                structure_type="nonexistent",
            )

        # Detect structure type and analyze content
        provider_months = self.discover_provider_months(stage_dir)
        total_files = sum(1 for _ in stage_dir.rglob("*.parquet"))

        if provider_months:
            structure_type = "provider_month_partitioned"
        elif any(
            p.is_dir() and p.name in ["AWS", "GCP", "Azure"]
            for p in stage_dir.iterdir()
        ):
            structure_type = "provider_partitioned"
        else:
            structure_type = "flat"

        return StageDirectoryInfo(
            stage_name=stage_name,
            stage_dir=stage_dir,
            run_dir=run_dir,
            structure_type=structure_type,
            provider_months=provider_months,
            total_files=total_files,
        )

    # === Organization Directory Management ===

    def get_org_directory(self, org_name: str) -> Path:
        """
        Get organization directory with auto-detection.

        Preserves existing behavior of finding org directories in multiple locations:
        1. Current directory orgs/ subdirectory
        2. cloudzero-demo-billing/orgs/ structure

        Args:
            org_name: Organization name

        Returns:
            Path to organization directory

        Raises:
            FileNotFoundError: If org directory cannot be found
        """
        # Try current directory structure first
        current_orgs = self.base_dir / "orgs"
        if current_orgs.exists():
            org_dir = current_orgs / org_name
            if org_dir.exists():
                return org_dir

        # Try cloudzero-demo-billing structure
        demo_billing = self.base_dir / "cloudzero-demo-billing" / "orgs"
        if demo_billing.exists():
            org_dir = demo_billing / org_name
            if org_dir.exists():
                return org_dir

        # Try parent directory patterns
        for parent_level in [1, 2]:
            parent_dir = self.base_dir
            for _ in range(parent_level):
                parent_dir = parent_dir.parent

            parent_orgs = parent_dir / "orgs"
            if parent_orgs.exists():
                org_dir = parent_orgs / org_name
                if org_dir.exists():
                    return org_dir

        # If not found, return path for creation (preserves existing behavior)
        default_org_dir = self.base_dir / "orgs" / org_name
        return default_org_dir

    def validate_organization_structure(self, org_name: str) -> Dict[str, bool]:
        """
        Validate organization directory structure.

        Checks for required directories and files in organization structure.
        Preserves existing validation behavior.

        Args:
            org_name: Organization name

        Returns:
            Dictionary mapping structure elements to existence status
        """
        try:
            org_dir = self.get_org_directory(org_name)
            paths = DirectoryPaths(org_dir)

            return {
                "org_directory": org_dir.exists(),
                "config_file": paths.config_file.exists(),
                "billing_directory": paths.billing_dir.exists(),
                "runs_directory": paths.runs_dir.exists(),
                "telemetry_directory": paths.telemetry_dir.exists(),
                "logs_directory": paths.logs_dir.parent.exists(),  # Check logs parent
            }
        except Exception:
            return {
                "org_directory": False,
                "config_file": False,
                "billing_directory": False,
                "runs_directory": False,
                "telemetry_directory": False,
                "logs_directory": False,
            }

    def create_organization_structure(self, org_name: str) -> DirectoryPaths:
        """
        Create complete organization directory structure.

        Creates all required directories for a new organization.

        Args:
            org_name: Organization name

        Returns:
            DirectoryPaths with created structure
        """
        org_dir = self.get_org_directory(org_name)
        paths = DirectoryPaths(org_dir)

        # Create all required directories
        paths.org_dir.mkdir(parents=True, exist_ok=True)
        paths.billing_dir.mkdir(parents=True, exist_ok=True)
        paths.runs_dir.mkdir(parents=True, exist_ok=True)
        paths.telemetry_dir.mkdir(parents=True, exist_ok=True)
        paths.logs_dir.mkdir(parents=True, exist_ok=True)

        return paths

    # === Utility Methods ===

    def extract_org_from_run_dir(self, run_dir: Union[str, Path]) -> str:
        """
        Extract organization name from run directory path.

        Preserves existing logic for extracting org names from run directory paths.

        Args:
            run_dir: Run directory path

        Returns:
            Organization name, or "default" if cannot be determined
        """
        run_path = Path(run_dir)
        path_parts = run_path.parts

        try:
            org_index = path_parts.index("orgs")
            if org_index + 1 < len(path_parts):
                return path_parts[org_index + 1]
        except (ValueError, IndexError):
            pass

        # Fallback pattern matching
        if "orgs" in str(run_path):
            path_str = str(run_path)
            if "/orgs/" in path_str:
                org_part = path_str.split("/orgs/")[1].split("/")[0]
                if org_part:
                    return org_part

        return "default"

    def cleanup_old_runs(self, org_name: str, keep_runs: int = 5) -> Dict[str, int]:
        """
        Clean up old run directories.

        Removes old run directories while keeping the most recent ones.

        Args:
            org_name: Organization name
            keep_runs: Number of recent runs to keep

        Returns:
            Dictionary with cleanup statistics
        """
        runs = self.list_all_run_directories(org_name)

        if len(runs) <= keep_runs:
            return {"total_runs": len(runs), "deleted_runs": 0, "kept_runs": len(runs)}

        # Keep most recent runs, delete older ones
        runs_to_delete = runs[keep_runs:]
        deleted_count = 0

        for run_info in runs_to_delete:
            try:
                # Remove run directory
                import shutil

                shutil.rmtree(run_info.run_dir)

                # Remove corresponding log directory
                org_dir = self.get_org_directory(org_name)
                paths = DirectoryPaths(org_dir)
                log_run_dir = paths.logs_dir / "runs" / run_info.run_id
                if log_run_dir.exists():
                    shutil.rmtree(log_run_dir)

                deleted_count += 1
            except Exception:
                # Continue cleanup even if individual deletion fails
                continue

        return {
            "total_runs": len(runs),
            "deleted_runs": deleted_count,
            "kept_runs": len(runs) - deleted_count,
        }

    # === Private Helper Methods ===

    def _ensure_base_directory_structure(self) -> None:
        """Ensure base directory exists and is accessible."""
        if not self.base_dir.exists():
            try:
                self.base_dir.mkdir(parents=True, exist_ok=True)
            except PermissionError:
                # If we can't create it, at least verify it's a valid path
                pass

    def _is_valid_run_timestamp(self, run_id: str) -> bool:
        """Check if run ID follows valid timestamp format."""
        # Pattern: YYYY-MM-DD_HHMMSS
        pattern = r"^\d{4}-\d{2}-\d{2}_\d{6}$"
        return bool(re.match(pattern, run_id))

    def _parse_run_timestamp(self, run_id: str) -> datetime:
        """Parse timestamp from run ID."""
        try:
            return datetime.strptime(run_id, "%Y-%m-%d_%H%M%S")
        except ValueError:
            # Fallback for non-standard formats
            return datetime.now()

    def _is_valid_month_format(self, month_str: str) -> bool:
        """Check if string is valid YYYY-MM month format."""
        try:
            if len(month_str) != 7 or month_str[4] != "-":
                return False
            year = int(month_str[:4])
            month = int(month_str[5:7])
            return 2020 <= year <= 2030 and 1 <= month <= 12
        except (ValueError, IndexError):
            return False

    def _detect_completed_stages(self, run_dir: Path) -> List[str]:
        """Detect which stages have been completed in a run."""
        stages = []
        common_stage_names = [
            "generate",
            "k8s",
            "metadata",
            "usage",
            "discount",
            "tax",
            "upload",
        ]

        for stage_name in common_stage_names:
            stage_dir = run_dir / stage_name
            if stage_dir.exists() and any(stage_dir.iterdir()):
                stages.append(stage_name)

        return stages

    def _generate_month_range(
        self, start_year: int = 2024, months_count: int = 12
    ) -> List[str]:
        """Generate reasonable month range for provider/month structure."""
        months = []
        current_year = start_year
        current_month = 1

        for _ in range(months_count):
            months.append(f"{current_year:04d}-{current_month:02d}")
            current_month += 1
            if current_month > 12:
                current_month = 1
                current_year += 1

        return months
