from __future__ import annotations

import json
import os
import shutil
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, Union, List
from pathlib import Path
from rich.console import Console
import toml

class BaseModel:
    """Base model class that provides dict-like access and JSON serialization."""
    
    def __init__(self, **data):
        # Initialize default values
        self.type = "app"
        self.name = "my-project"
        self.version = "0.1.0"
        self.description = ""
        self.authors = data.get('authors', [{"name": "PyDM User"}])
        self.license = data.get('license', "MIT")
        self.dependencies = data.get('dependencies', {})
        self.optionalDependencies = data.get('optionalDependencies', {})
        self.scripts = data.get('scripts', {})
        self.useGlobalDeps = data.get('useGlobalDeps', False)
        self.pyprojectUse = data.get('pyprojectUse')
        self.executable = data.get('executable')
        
        # Only include these fields for module type by default
        if data.get('type') == 'module':
            self.cli = data.get('cli', {})
            self.pyproject = data.get('pyproject', {})
        else:
            # For app type, only include if explicitly provided
            self.cli = data.get('cli')
            self.pyproject = data.get('pyproject')
        
        # Set type after checking it in the conditions above
        self.type = data.get('type', 'app')
        
        # Update with any remaining values
        for key, value in data.items():
            if not hasattr(self, key):
                setattr(self, key, value)
    
    def dict(self):
        """Convert the model to a dictionary."""
        result = {}
        for k, v in self.__dict__.items():
            if k.startswith('_'):
                continue
                
            # Only include cli and pyproject for module type or if explicitly set
            if k in ['cli', 'pyproject']:
                if self.type != 'module' and getattr(self, k, None) is None:
                    continue
                    
            result[k] = v
            
        return result
    
    def json(self, **kwargs):
        """Convert the model to a JSON string."""
        import json
        return json.dumps(self.dict(), **kwargs)

console = Console()
CONFIG_DIR = ".pydmconf"

def get_config_dir(create: bool = True) -> Path:
    """Get the path to the config directory, optionally creating it if it doesn't exist.
    
    Args:
        create: If True, create the directory if it doesn't exist
    """
    path = Path.cwd() / CONFIG_DIR
    if create and not path.exists():
        path.mkdir(exist_ok=True, parents=True)
    return path

def get_pyproject_path(outdir: Optional[Union[str, Path]] = None) -> Path:
    """Get the path to the pyproject.toml file.
    
    Args:
        outdir: If specified, use this directory instead of the config directory
    """
    if outdir is not None:
        outdir = Path(outdir)
        outdir.mkdir(parents=True, exist_ok=True)
        return outdir / "pyproject.toml"
    return get_config_dir() / "pyproject.toml"

def ensure_pyproject() -> Optional[Path]:
    """Ensure the pyproject.toml exists in the config directory.
    
    Returns:
        Path to the pyproject.toml file, or None if it couldn't be created
    """
    pyproject_path = get_pyproject_path()
    if not pyproject_path.exists():
        try:
            pkg = PyPackage.load()
            pkg.to_pyproject(pyproject_path)
            return pyproject_path
        except Exception as e:
            console.print(f"[yellow]Warning: Could not generate pyproject.toml: {e}")
            return None
    return pyproject_path

DEFAULT_PYPACKAGE = {
    "type": "app",
    "name": "my-app",
    "version": "0.1.0",
    "description": "This is a simple application autogenerated with PyDM.",
    "authors": [
        {"name": "PyDM User"}
    ],
    "license": "MIT",
    "dependencies": {},
    "scripts": {
        "dev": "python main.py"
    },
    "useGlobalDeps": False,
    # pyprojectUse is not included in default for app type (defaults to False)
    "executable": {
        "target": "main.py",
        "parameters": ["--onefile"],
        "output": "dist/executable"
    },
    # cli and pyproject sections are not included by default for app type
}


@dataclass
class PyPackage(BaseModel):
    """A Python package configuration."""
    
    type: str = "app"
    name: str = "my-project"
    version: str = "0.1.0"
    description: str = ""
    authors: List[Dict[str, str]] = field(default_factory=lambda: [{"name": "PyDM User"}])
    license: str = "MIT"
    dependencies: Dict[str, str] = field(default_factory=dict)
    optionalDependencies: Dict[str, Dict[str, str]] = field(default_factory=dict)
    scripts: Dict[str, str] = field(default_factory=dict)
    cli: Optional[Dict[str, Any]] = None
    pyproject: Optional[Dict[str, Any]] = None
    useGlobalDeps: bool = False
    pyprojectUse: Optional[bool] = None
    executable: Optional[Dict[str, Any]] = None

    @staticmethod
    def path(cwd: Optional[str] = None) -> Path:
        base = Path(cwd or os.getcwd())
        return base / "pypackage.json"

    @staticmethod
    def lockfile_path(cwd: Optional[str] = None) -> Path:
        base = Path(cwd or os.getcwd())
        return base / "pypackage-lock.json"

    @classmethod
    def load(cls, cwd: Optional[str] = None) -> "PyPackage":
        path = cls.path(cwd)
        if not path.exists():
            raise FileNotFoundError("pypackage.json not found. Run 'pydm init' or 'pydm new'.")
        data = json.loads(path.read_text(encoding="utf-8"))
        return cls(**data)

    def save(self, cwd: Optional[str] = None) -> None:
        path = self.path(cwd)
        data = {
            "type": self.type,
            "name": self.name,
            "version": self.version,
            "description": self.description,
            "authors": self.authors,
            "license": self.license,
            "dependencies": self.dependencies,
            "scripts": self.scripts,
            "cli": self.cli,
            "pyproject": self.pyproject,
            "useGlobalDeps": self.useGlobalDeps,
            "pyprojectUse": self.pyprojectUse if self.type == "app" else None  # Only include for app type
        }
        
        # Only include executable if it exists and is not None
        if self.executable is not None:
            data["executable"] = self.executable
            
        # remove Nones and empty keys for cleanliness
        data = {k: v for k, v in data.items() if v not in (None, [])}
        path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
        
    def get_pyproject_content(self):
        """Generate the content for pyproject.toml as a dictionary.
        
        Returns:
            Dictionary containing the pyproject.toml content
        """
        # Start with build system requirements
        pyproject = {
            "build-system": {
                "requires": ["setuptools>=42", "wheel"],
                "build-backend": "setuptools.build_meta"
            },
            "project": {
                "name": self.name.replace("-", "_"),  # Ensure valid Python package name
                "version": self.version,
                "description": getattr(self, 'description', ''),
                "authors": getattr(self, 'authors', []),
                "dependencies": [f"{pkg}{ver}" if ver and ver != "*" else pkg 
                               for pkg, ver in getattr(self, 'dependencies', {}).items()]
            }
        }
        
        # Add license if available
        if hasattr(self, 'license') and self.license:
            pyproject["project"]["license"] = {"text": self.license}
        
        # Handle custom pyproject content if provided
        if hasattr(self, 'pyproject') and self.pyproject and isinstance(self.pyproject, dict):
            # Process structured format (sections with [header] keys)
            structured_content = {}
            for key, value in self.pyproject.items():
                if isinstance(key, str) and key.startswith('[') and key.endswith(']'):
                    section = key[1:-1]  # Remove brackets
                    parts = section.split('.')
                    current = structured_content
                    for part in parts[:-1]:
                        if part not in current:
                            current[part] = {}
                        current = current[part]
                    current[parts[-1]] = value
            
            # Deep merge structured content
            def deep_merge(dest, source):
                for key, value in source.items():
                    if key in dest and isinstance(dest[key], dict) and isinstance(value, dict):
                        deep_merge(dest[key], value)
                    else:
                        dest[key] = value
            
            deep_merge(pyproject, structured_content)
            
            # Handle raw content if provided
            if "_raw" in self.pyproject and isinstance(self.pyproject["_raw"], str):
                try:
                    import toml
                    raw_content = toml.loads(self.pyproject["_raw"])
                    deep_merge(pyproject, raw_content)
                except Exception as e:
                    console.print(f"[yellow]Warning: Could not parse _raw pyproject content: {e}")
        
        # Add dependencies
        if hasattr(self, 'dependencies') and self.dependencies:
            pyproject["project"]["dependencies"] = [
                f"{pkg}{ver}" if ver and ver != "*" else pkg 
                for pkg, ver in self.dependencies.items()
            ]
        
        # Add optional dependencies
        if hasattr(self, 'optionalDependencies') and self.optionalDependencies:
            if "optional-dependencies" not in pyproject["project"]:
                pyproject["project"]["optional-dependencies"] = {}
            
            for group, deps in self.optionalDependencies.items():
                if group not in pyproject["project"]["optional-dependencies"]:
                    pyproject["project"]["optional-dependencies"][group] = []
                
                for pkg, spec in deps.items():
                    if spec and spec != "*":
                        pyproject["project"]["optional-dependencies"][group].append(f"{pkg}{spec}")
                    else:
                        pyproject["project"]["optional-dependencies"][group].append(pkg)
        
        # Add CLI entry points
        if hasattr(self, 'cli') and self.cli and isinstance(self.cli, dict):
            if "scripts" not in pyproject["project"]:
                pyproject["project"]["scripts"] = {}
            
            for cmd_name, cmd_info in self.cli.items():
                if isinstance(cmd_info, dict) and "target" in cmd_info:
                    pyproject["project"]["scripts"][cmd_name] = cmd_info["target"]
                elif isinstance(cmd_info, str):
                    pyproject["project"]["scripts"][cmd_name] = cmd_info
        
        return pyproject

    def to_pyproject(self, path: Optional[Union[str, Path]] = None, *, skip_validation: bool = False) -> Path:
        """Generate a pyproject.toml file from this package.
        
        Args:
            path: Path where to save the pyproject.toml file. If None, uses the config directory.
            skip_validation: If True, skips TOML validation (useful for custom content)
            
        Returns:
            Path to the generated pyproject.toml file
        """
        if path is None:
            path = self.path().parent / ".pydmconf" / "pyproject.toml"
        else:
            path = Path(path)
            
        path.parent.mkdir(parents=True, exist_ok=True)
        
        # If there's custom raw content, write it directly
        if hasattr(self, 'pyproject') and isinstance(self.pyproject, dict) and "_raw" in self.pyproject:
            try:
                if isinstance(self.pyproject["_raw"], str):
                    path.write_text(self.pyproject["_raw"], encoding='utf-8')
                    return path
            except Exception as e:
                console.print(f"[yellow]Warning: Could not write raw pyproject content: {e}")
                console.print("[yellow]Falling back to generated content...")
        
        # Generate and write the pyproject content
        try:
            import toml
            pyproject = self.get_pyproject_content()
            
            # Write the generated content
            with open(path, 'w', encoding='utf-8') as f:
                toml.dump(pyproject, f)
            
            # Validate the generated TOML if not skipping validation
            if not skip_validation and self.type == "module":
                try:
                    with open(path, 'r', encoding='utf-8') as f:
                        toml.load(f)  # This will raise if invalid
                except Exception as e:
                    console.print(f"[yellow]Warning: Generated pyproject.toml may be invalid: {e}")
            
            return path
            
        except Exception as e:
            console.print(f"[red]Error generating pyproject.toml: {e}")
            raise


def init_pypackage(cwd: Optional[str] = None, pkg_type: str = "app", name: Optional[str] = None, globaldeps: bool = False, pyproject_use: Optional[bool] = None) -> PyPackage:
    path = PyPackage.path(cwd)
    if path.exists():
        console.print("[yellow]pypackage.json already exists. Skipping creation.")
        return PyPackage.load(cwd)
    
    # Start with a clean slate with only common fields
    data = {
        "type": pkg_type,
        "name": name or "my-project",
        "version": "0.1.0",
        "description": "",
        "authors": [{"name": "PyDM User"}],
        "license": "MIT",
        "dependencies": {},
        "useGlobalDeps": globaldeps,
        # Initialize pyproject with empty dict for module type
        "pyproject": {} if pkg_type == "module" else None,
        # Initialize scripts with empty dict, will be populated based on project type
        "scripts": {}
    }
    
    # Type-specific settings
    if pkg_type == "module":
        module_name = name.replace("-", "_") if name else "mymodule"
        data.update({
            "description": "This is a simple module autogenerated with PyDM.",
            "scripts": {
                "start": f"python -m {module_name}"
            },
            "cli": {
                module_name: {
                    "name": module_name,
                    "target": f"{module_name}.main:main"
                }
            },
            "pyprojectUse": bool(pyproject_use)
        })
        
        # Only include pyproject section if explicitly enabled
        if data["pyprojectUse"]:
            data["pyproject"] = {
                "build-system": {
                    "requires": ["setuptools>=42", "wheel"],
                    "build-backend": "setuptools.build_meta"
                }
            }

            
    elif pkg_type == "app":
        data.update({
            "description": "This is a simple application autogenerated with PyDM.",
            "scripts": {
                "dev": "python main.py",
                "start": "python main.py"
            },
            "executable": {
                "target": "main.py",
                "parameters": ["--onefile"],
                "output": "dist/executable"
            },
            "pyprojectUse": bool(pyproject_use)
        })
        
        # Only include pyproject section if explicitly enabled
        if data["pyprojectUse"]:
            data["pyproject"] = {
                "build-system": {
                    "requires": ["setuptools>=42", "wheel"],
                    "build-backend": "setuptools.build_meta"
                },
                "project": {
                    "entry-points": {
                        "console_scripts": [
                            f"{module_name} = {module_name}.main:main"
                        ]
                    }
                }
            }
    
    if globaldeps:
        data["useGlobalDeps"] = True
    
    pkg = PyPackage(**data)
    pkg.save(cwd)
        # Create default module structure for module type
    if pkg_type == "module":
        module_dir = Path(cwd or ".") / module_name
        module_dir.mkdir(exist_ok=True)
        
        # Create __init__.py
        (module_dir / "__init__.py").write_text(
            f'"""{data["description"]}"""\n'
            f'__version__ = "{data["version"]}"\n',
            encoding="utf-8"
        )
        
        # Create main.py with a simple hello message
        main_content = '''"""Main entry point for the module."""

def main():
    """Main function for CLI execution."""
    print("Hello from PyDM!")

if __name__ == "__main__":
    main()
'''
        (module_dir / "main.py").write_text(main_content, encoding="utf-8")
        
        console.print(f"[green]Created module structure in {module_dir}")
    
    console.print(f"[green]Created {path}")
    return pkg
