"""Project environment bootstrapping and test scaffolding.

Detects project types (including 1-level-deep subfolders), ensures environments
are properly initialized (venv, deps), and scaffolds sample tests when missing.
"""

import os
import subprocess
import shutil
import json
from pathlib import Path
from typing import Dict, List, Optional, Tuple

import click

try:
    from .project_doctor import diagnose_and_report
except ImportError:
    from project_doctor import diagnose_and_report


# =============================================================================
# Project bootstrap configuration per language/framework
# =============================================================================

PROJECT_BOOTSTRAP = {
    'python': {
        'marker_files': ['pyproject.toml', 'setup.py', 'setup.cfg', 'requirements.txt'],
        'env_dir': '.venv',
        'test_dirs': ['tests', 'test'],
        'test_patterns': ['test_*.py', '*_test.py'],
        'dep_install_commands': [
            # Order matters: try pyproject.toml editable install first, then requirements.txt
            {'condition': 'pyproject.toml', 'cmd': '{python} -m pip install -e ".[dev]"', 'fallback': '{python} -m pip install -e .'},
            {'condition': 'requirements.txt', 'cmd': '{python} -m pip install -r requirements.txt'},
            {'condition': 'requirements-dev.txt', 'cmd': '{python} -m pip install -r requirements-dev.txt'},
        ],
        'test_command': 'pytest',
        'test_dep': 'pytest',
        'scaffold_test': '''"""Sample test — auto-generated by goal."""


def test_placeholder():
    """Placeholder test to verify the test setup works."""
    assert True


def test_import():
    """Verify the main package can be imported."""
    import {package_name}  # noqa: F401
''',
    },
    'nodejs': {
        'marker_files': ['package.json'],
        'env_dir': 'node_modules',
        'test_dirs': ['test', 'tests', '__tests__', 'spec'],
        'test_patterns': ['*.test.js', '*.spec.js', '*.test.ts', '*.spec.ts'],
        'dep_install_commands': [
            {'condition': 'yarn.lock', 'cmd': 'yarn install'},
            {'condition': 'pnpm-lock.yaml', 'cmd': 'pnpm install'},
            {'condition': 'package-lock.json', 'cmd': 'npm install'},
            {'condition': 'package.json', 'cmd': 'npm install'},
        ],
        'test_command': 'npm test',
        'test_dep': 'jest',
        'scaffold_test': '''// Sample test — auto-generated by goal.

describe('{package_name}', () => {{
  test('placeholder test', () => {{
    expect(true).toBe(true);
  }});
}});
''',
    },
    'rust': {
        'marker_files': ['Cargo.toml'],
        'env_dir': 'target',
        'test_dirs': ['tests'],
        'test_patterns': ['*.rs'],
        'dep_install_commands': [
            {'condition': 'Cargo.toml', 'cmd': 'cargo build'},
        ],
        'test_command': 'cargo test',
        'test_dep': None,
        'scaffold_test': '''// Sample test — auto-generated by goal.

#[cfg(test)]
mod tests {{
    #[test]
    fn placeholder_test() {{
        assert!(true);
    }}
}}
''',
    },
    'go': {
        'marker_files': ['go.mod'],
        'env_dir': None,
        'test_dirs': ['.'],
        'test_patterns': ['*_test.go'],
        'dep_install_commands': [
            {'condition': 'go.mod', 'cmd': 'go mod download'},
        ],
        'test_command': 'go test ./...',
        'test_dep': None,
        'scaffold_test': '''// Sample test — auto-generated by goal.
package main

import "testing"

func TestPlaceholder(t *testing.T) {{
\tif false {{
\t\tt.Error("placeholder test failed")
\t}}
}}
''',
    },
    'ruby': {
        'marker_files': ['Gemfile', '*.gemspec'],
        'env_dir': 'vendor/bundle',
        'test_dirs': ['spec', 'test'],
        'test_patterns': ['*_spec.rb', 'test_*.rb', '*_test.rb'],
        'dep_install_commands': [
            {'condition': 'Gemfile', 'cmd': 'bundle install'},
        ],
        'test_command': 'bundle exec rspec',
        'test_dep': 'rspec',
        'scaffold_test': '''# Sample test — auto-generated by goal.

RSpec.describe '{package_name}' do
  it 'placeholder test' do
    expect(true).to be true
  end
end
''',
    },
    'php': {
        'marker_files': ['composer.json'],
        'env_dir': 'vendor',
        'test_dirs': ['tests', 'test'],
        'test_patterns': ['*Test.php', 'test_*.php'],
        'dep_install_commands': [
            {'condition': 'composer.json', 'cmd': 'composer install'},
        ],
        'test_command': 'composer test',
        'test_dep': 'phpunit/phpunit',
        'scaffold_test': '''<?php
// Sample test — auto-generated by goal.

use PHPUnit\\Framework\\TestCase;

class PlaceholderTest extends TestCase
{{
    public function testPlaceholder(): void
    {{
        $this->assertTrue(true);
    }}
}}
''',
    },
    'dotnet': {
        'marker_files': ['*.csproj', '*.fsproj', '*.sln'],
        'env_dir': 'bin',
        'test_dirs': ['Tests', 'tests'],
        'test_patterns': ['*Tests.cs', '*Test.cs'],
        'dep_install_commands': [
            {'condition': '*.csproj', 'cmd': 'dotnet restore'},
            {'condition': '*.sln', 'cmd': 'dotnet restore'},
        ],
        'test_command': 'dotnet test',
        'test_dep': None,
        'scaffold_test': '''// Sample test — auto-generated by goal.
using Xunit;

namespace {package_name}.Tests
{{
    public class PlaceholderTest
    {{
        [Fact]
        public void Placeholder_ShouldPass()
        {{
            Assert.True(true);
        }}
    }}
}}
''',
    },
    'java': {
        'marker_files': ['pom.xml', 'build.gradle', 'build.gradle.kts'],
        'env_dir': 'target',
        'test_dirs': ['src/test/java', 'src/test'],
        'test_patterns': ['*Test.java', '*Tests.java'],
        'dep_install_commands': [
            {'condition': 'pom.xml', 'cmd': 'mvn dependency:resolve'},
            {'condition': 'build.gradle', 'cmd': 'gradle dependencies'},
            {'condition': 'build.gradle.kts', 'cmd': 'gradle dependencies'},
        ],
        'test_command': 'mvn test',
        'test_dep': None,
        'scaffold_test': '''// Sample test — auto-generated by goal.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PlaceholderTest {{
    @Test
    void placeholderTest() {{
        assertTrue(true);
    }}
}}
''',
    },
}


# =============================================================================
# Detection helpers
# =============================================================================

def _match_marker(base: Path, pattern: str) -> bool:
    """Check if a marker file/pattern exists under *base*."""
    if '*' in pattern:
        return bool(list(base.glob(pattern)))
    return (base / pattern).exists()


def detect_project_types_deep(root: Optional[Path] = None, max_depth: int = 1) -> Dict[str, List[Path]]:
    """Detect project types in *root* and up to *max_depth* subfolder levels.

    Returns a dict mapping project type → list of directories where it was found.
    """
    if root is None:
        root = Path('.')
    root = root.resolve()

    results: Dict[str, List[Path]] = {}

    dirs_to_scan = [root]
    if max_depth >= 1:
        try:
            for child in sorted(root.iterdir()):
                if child.is_dir() and not child.name.startswith('.'):
                    dirs_to_scan.append(child)
        except PermissionError:
            pass

    for scan_dir in dirs_to_scan:
        for ptype, cfg in PROJECT_BOOTSTRAP.items():
            for marker in cfg['marker_files']:
                if _match_marker(scan_dir, marker):
                    results.setdefault(ptype, []).append(scan_dir)
                    break

    return results


def guess_package_name(project_dir: Path, project_type: str) -> str:
    """Best-effort guess of the package/module name for scaffold templates."""
    project_dir = project_dir.resolve()

    if project_type == 'python':
        # Try pyproject.toml name field
        pyproject = project_dir / 'pyproject.toml'
        if pyproject.exists():
            try:
                import re
                content = pyproject.read_text(errors='ignore')
                m = re.search(r'^name\s*=\s*["\']([^"\']+)["\']', content, re.MULTILINE)
                if m:
                    return m.group(1).replace('-', '_')
            except Exception:
                pass
        # Try setup.py
        setup_py = project_dir / 'setup.py'
        if setup_py.exists():
            try:
                import re
                content = setup_py.read_text(errors='ignore')
                m = re.search(r'name\s*=\s*["\']([^"\']+)["\']', content)
                if m:
                    return m.group(1).replace('-', '_')
            except Exception:
                pass

    elif project_type == 'nodejs':
        pkg = project_dir / 'package.json'
        if pkg.exists():
            try:
                data = json.loads(pkg.read_text(errors='ignore'))
                name = data.get('name', '')
                return name.split('/')[-1] if '/' in name else name
            except Exception:
                pass

    elif project_type == 'rust':
        cargo = project_dir / 'Cargo.toml'
        if cargo.exists():
            try:
                import re
                content = cargo.read_text(errors='ignore')
                m = re.search(r'^name\s*=\s*"([^"]+)"', content, re.MULTILINE)
                if m:
                    return m.group(1)
            except Exception:
                pass

    elif project_type == 'go':
        gomod = project_dir / 'go.mod'
        if gomod.exists():
            try:
                first_line = gomod.read_text(errors='ignore').split('\n')[0]
                parts = first_line.strip().split()
                if len(parts) >= 2:
                    return parts[1].rsplit('/', 1)[-1]
            except Exception:
                pass

    # Fallback: directory name
    return project_dir.name.replace('-', '_')


# =============================================================================
# Environment bootstrapping
# =============================================================================

def _find_python_bin(project_dir: Path) -> str:
    """Return the best python binary path for a project directory."""
    venv_python = project_dir / '.venv' / 'bin' / 'python'
    if venv_python.exists():
        return str(venv_python)
    # Check active virtualenv
    venv_env = os.environ.get('VIRTUAL_ENV')
    if venv_env:
        candidate = Path(venv_env) / 'bin' / 'python'
        if candidate.exists():
            return str(candidate)
    # sys.executable
    import sys
    return sys.executable


def ensure_project_environment(project_dir: Path, project_type: str, yes: bool = False) -> bool:
    """Ensure the project environment is properly set up.

    For Python: creates .venv if missing, installs dependencies.
    For Node: runs npm/yarn/pnpm install if node_modules missing.
    For others: runs the appropriate dependency install command.

    Returns True if environment is ready, False on failure.
    """
    cfg = PROJECT_BOOTSTRAP.get(project_type)
    if not cfg:
        return True

    project_dir = project_dir.resolve()
    env_dir = cfg.get('env_dir')

    # --- Python special handling: create venv if missing ---
    if project_type == 'python':
        venv_path = project_dir / '.venv'
        if not venv_path.exists():
            if not yes:
                click.echo(click.style(f"\n⚠  No virtual environment found in {project_dir}", fg='yellow'))
                if not click.confirm(click.style("Create .venv and install dependencies?", fg='cyan'), default=True):
                    return True  # user declined, not a failure
            click.echo(click.style(f"  Creating .venv in {project_dir} ...", fg='cyan'))
            import sys
            result = subprocess.run(
                [sys.executable, '-m', 'venv', str(venv_path)],
                capture_output=True, text=True
            )
            if result.returncode != 0:
                click.echo(click.style(f"  ✗ Failed to create venv: {result.stderr.strip()}", fg='red'))
                return False
            click.echo(click.style("  ✓ Created .venv", fg='green'))

        python_bin = _find_python_bin(project_dir)

        # Upgrade pip
        subprocess.run([python_bin, '-m', 'pip', 'install', '--upgrade', 'pip'],
                       capture_output=True, text=True, cwd=str(project_dir))

        # Install deps
        for dep_cfg in cfg['dep_install_commands']:
            condition_file = dep_cfg['condition']
            if _match_marker(project_dir, condition_file):
                cmd = dep_cfg['cmd'].format(python=python_bin)
                click.echo(click.style(f"  Installing deps: {cmd}", fg='cyan'))
                result = subprocess.run(
                    cmd, shell=True, cwd=str(project_dir),
                    capture_output=True, text=True
                )
                if result.returncode != 0:
                    # Try fallback if available
                    fallback = dep_cfg.get('fallback')
                    if fallback:
                        cmd = fallback.format(python=python_bin)
                        click.echo(click.style(f"  Retrying: {cmd}", fg='yellow'))
                        result = subprocess.run(
                            cmd, shell=True, cwd=str(project_dir),
                            capture_output=True, text=True
                        )
                    if result.returncode != 0:
                        click.echo(click.style(f"  ⚠  Dependency install had issues (exit {result.returncode})", fg='yellow'))
                        # Not fatal — some deps may be optional
                else:
                    click.echo(click.style("  ✓ Dependencies installed", fg='green'))
                break  # Only run the first matching dep install
        return True

    # --- Generic handling for other project types ---
    if env_dir:
        env_path = project_dir / env_dir
        if env_path.exists():
            return True  # Already set up

    # Check if deps need installing
    needs_install = env_dir and not (project_dir / env_dir).exists()
    if not needs_install:
        # Check if any condition file exists but env is missing
        for dep_cfg in cfg['dep_install_commands']:
            if _match_marker(project_dir, dep_cfg['condition']):
                needs_install = True
                break

    if needs_install:
        if not yes:
            click.echo(click.style(f"\n⚠  Dependencies not installed for {project_type} project in {project_dir}", fg='yellow'))
            if not click.confirm(click.style("Install dependencies?", fg='cyan'), default=True):
                return True

        for dep_cfg in cfg['dep_install_commands']:
            if _match_marker(project_dir, dep_cfg['condition']):
                cmd = dep_cfg['cmd']
                tool = cmd.split()[0]
                if not shutil.which(tool):
                    click.echo(click.style(f"  ⚠  '{tool}' not found in PATH, skipping", fg='yellow'))
                    continue
                click.echo(click.style(f"  Installing deps: {cmd}", fg='cyan'))
                result = subprocess.run(
                    cmd, shell=True, cwd=str(project_dir),
                    capture_output=True, text=True
                )
                if result.returncode == 0:
                    click.echo(click.style("  ✓ Dependencies installed", fg='green'))
                else:
                    click.echo(click.style(f"  ⚠  Install had issues (exit {result.returncode})", fg='yellow'))
                break

    return True


# =============================================================================
# Test scaffolding
# =============================================================================

def find_existing_tests(project_dir: Path, project_type: str) -> List[Path]:
    """Find existing test files for the given project type."""
    cfg = PROJECT_BOOTSTRAP.get(project_type)
    if not cfg:
        return []

    found: List[Path] = []
    for test_dir_name in cfg['test_dirs']:
        test_dir = project_dir / test_dir_name
        if not test_dir.exists():
            continue
        for pattern in cfg['test_patterns']:
            found.extend(test_dir.rglob(pattern))

    return found


def scaffold_test(project_dir: Path, project_type: str, yes: bool = False) -> Optional[Path]:
    """Create a sample test file if no tests exist.

    Returns the path to the created test file, or None if skipped/not needed.
    """
    cfg = PROJECT_BOOTSTRAP.get(project_type)
    if not cfg or not cfg.get('scaffold_test'):
        return None

    existing = find_existing_tests(project_dir, project_type)
    if existing:
        return None  # Tests already exist

    if not yes:
        click.echo(click.style(f"\n⚠  No tests found for {project_type} project in {project_dir}", fg='yellow'))
        if not click.confirm(click.style("Create a sample test file?", fg='cyan'), default=True):
            return None

    package_name = guess_package_name(project_dir, project_type)

    # Determine test directory and file name
    test_dir_name = cfg['test_dirs'][0]
    test_dir = project_dir / test_dir_name

    if project_type == 'python':
        test_file = test_dir / f'test_{package_name}.py'
    elif project_type == 'nodejs':
        test_file = test_dir / f'{package_name}.test.js'
    elif project_type == 'rust':
        test_file = test_dir / 'placeholder_test.rs'
    elif project_type == 'go':
        test_file = project_dir / 'placeholder_test.go'
    elif project_type == 'ruby':
        test_file = test_dir / f'{package_name}_spec.rb'
    elif project_type == 'php':
        test_file = test_dir / 'PlaceholderTest.php'
    elif project_type == 'dotnet':
        test_file = test_dir / 'PlaceholderTest.cs'
    elif project_type == 'java':
        test_file = test_dir / 'PlaceholderTest.java'
    else:
        return None

    if test_file.exists():
        return None

    test_dir.mkdir(parents=True, exist_ok=True)
    content = cfg['scaffold_test'].format(package_name=package_name)
    test_file.write_text(content, encoding='utf-8')

    click.echo(click.style(f"  ✓ Created sample test: {test_file.relative_to(project_dir)}", fg='green'))
    return test_file


# =============================================================================
# High-level orchestration
# =============================================================================

def bootstrap_project(project_dir: Path, project_type: str, yes: bool = False) -> Dict:
    """Full bootstrap: diagnose & fix config, ensure environment, scaffold tests.

    Returns a dict with status info:
        {
            'project_type': str,
            'project_dir': Path,
            'env_ok': bool,
            'tests_found': list[Path],
            'test_created': Path | None,
            'doctor_report': DoctorReport | None,
        }
    """
    result = {
        'project_type': project_type,
        'project_dir': project_dir,
        'env_ok': False,
        'tests_found': [],
        'test_created': None,
        'doctor_report': None,
    }

    # Step 1: Diagnose and auto-fix project configuration issues
    result['doctor_report'] = diagnose_and_report(project_dir, project_type, auto_fix=yes)

    # Step 2: Ensure environment (venv, deps)
    result['env_ok'] = ensure_project_environment(project_dir, project_type, yes=yes)

    # Step 3: Find or scaffold tests
    result['tests_found'] = find_existing_tests(project_dir, project_type)

    if not result['tests_found']:
        result['test_created'] = scaffold_test(project_dir, project_type, yes=yes)
        if result['test_created']:
            result['tests_found'] = [result['test_created']]

    return result


def bootstrap_all_projects(root: Optional[Path] = None, yes: bool = False) -> List[Dict]:
    """Detect all project types (root + 1-level subfolders) and bootstrap each.

    Returns a list of bootstrap result dicts.
    """
    detected = detect_project_types_deep(root)
    if not detected:
        return []

    results = []
    for ptype, dirs in detected.items():
        for project_dir in dirs:
            try:
                rel = project_dir.relative_to(root.resolve() if root else Path('.').resolve())
            except ValueError:
                rel = project_dir
            click.echo(click.style(f"\n📦 Bootstrapping {ptype} project in {rel}", fg='cyan', bold=True))
            res = bootstrap_project(project_dir, ptype, yes=yes)
            results.append(res)

    return results
