import dataclasses
import io
import glob
import os
import pathlib
import logging
import sys
import subprocess
import shutil
import abc

from .os import shutil_which

from typing import (
    Optional,
    Literal,
    Any,
)

logger = logging.getLogger(__name__)

@dataclasses.dataclass
class Project:
    source_dir : pathlib.Path
    build_dir : pathlib.Path
    dest_dir : pathlib.Path
    meson_path: Optional[pathlib.Path] = None

@dataclasses.dataclass
class Dependency:
    name: str
    mode : Literal['pyproject', 'meson', 'meson-python', 'm']
    source_path : pathlib.Path
    args: Optional[list[str]] = None

@dataclasses.dataclass
class DistSettings:
    wheel_dir : pathlib.Path
    python_path: pathlib.Path
    env_path: pathlib.Path

class CLI(abc.ABC):
    @property
    @abc.abstractmethod
    def dist_settings(self) -> DistSettings:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def projects(self) -> dict[str, Project]:
        raise NotImplementedError

    @property
    @abc.abstractmethod
    def dependencies(self) -> dict[str, Dependency]:
        raise NotImplementedError

    def mypy(
        self,
        argv: list[str]
    ) -> None:
        from . import mypy as _mypy

        _mypy.run(
            argv,
        )

    def ruff(
        self,
        project_name: str,
        argv: list[str],
    ) -> None:
        project = self.projects[project_name]

        if len(argv) == 0:
            argv = ['check', '.',]

        subprocess.check_call([
            self.dist_settings.python_path,
            '-m',
            'ruff',
            '--config', str(project.source_dir / 'pyproject.toml'),
            *argv,
        ])

    def pyright(
        self,
        project_name: str,
        argv: list[str],
    ) -> None:
        project = self.projects[project_name]

        if len(argv) == 0:
            argv = ['--threads', '3', '.']

        subprocess.check_call([
            self.dist_settings.python_path,
            '-m',
            'pyright',
            '-p', str(project.source_dir / 'pyproject.toml'),
            *argv,
        ])

    def pip_sync(
        self,
        project: str,
        features: list[str],
    ) -> None:
        from . import cli_bootstrap

        pyproject = cli_bootstrap.pyproject_load(
            self.projects[project].source_dir / 'pyproject.toml'
        )

        dependencies = sum([
            pyproject.dependencies[o]
            for o in features
        ], [])

        pip_find_links : list[pathlib.Path] = []

        if not pyproject.pip_find_links is None:
            pip_find_links.extend(pyproject.pip_find_links)


        logger.info(dict(
            dependencies=dependencies,
        ))

        if len(dependencies) > 0:
            subprocess.check_call([
                self.dist_settings.python_path,
                '-m',
                'uv', 'pip', 'install',
                *sum([
                    ['-f', str(o),]
                    for o in pip_find_links
                ], []),
                # '-f', str(pathlib.Path(__file__).parent / 'deps' / 'dist'),
                '--offline',
                *dependencies,
            ])

    def deploy_fetch_dist(
        self,
        force: bool,
    ) -> None:
        for k, d in self.dependencies.items():
            whl_glob = self.dist_settings.wheel_dir / ('*%s*.whl' % d.name.replace('.', '_'))
            if len(glob.glob(
                str(whl_glob)
            )) == 0 or force:
                if d.source_path.exists():
                    def whl_files_get() -> list[dict[str, Any]]:
                        return [
                            dict(
                                path=o,
                                stat=os.stat(o).st_mtime,
                            )
                            for o in glob.glob(
                                str(whl_glob)
                            )
                        ]

                    present_files = whl_files_get()

                    if d.mode == 'm':
                        if (d.source_path / 'm.py').exists():
                            cmd = [
                                sys.executable,
                                str(d.source_path / 'm.py'),
                                'deploy:wheel',
                                '-o',
                                str(self.dist_settings.wheel_dir),
                            ]

                            if not d.args is None:
                                cmd.extend(d.args)

                            subprocess.check_call(
                                cmd,
                                cwd=d.source_path,
                            )
                    else:
                        raise NotImplementedError

                    updated_files = whl_files_get()

                    def index_get(o: dict[str, Any]) -> tuple[Any, ...]:
                        return (o['path'], o['stat'])

                    present_files_index = {
                        index_get(o) : o
                        for o in present_files
                    }

                    new_files : list[dict[str, Any]] = []

                    for o in updated_files:
                        entry_index = index_get(o)

                        if not entry_index in present_files_index:
                            new_files.append(o)

                    if len(new_files) == 0:
                        raise NotImplementedError

                    latest_file = sorted(
                        new_files,
                        key=lambda x: x['stat']
                    )[-1]

                    subprocess.check_call([
                        self.dist_settings.python_path,
                        '-m', 'pip',
                        'install', 
                        latest_file['path'],
                    ])

    @property
    def pkg_config_path(self,) -> set[pathlib.Path]:
        return {
            pathlib.Path(o)
            for o in glob.glob(
                str(self.dist_settings.env_path / 'lib' / 'python*' / '**' / 'pkgconfig'),
                recursive=True,
            )
        }

    def deploy_wheel(
        self,
        project_name: str,
        argv: Optional[list[str]] = None,
        output_dir: Optional[pathlib.Path] = None,
        force: Optional[bool] = None,
        env: Optional[dict[str, str]] = None,
        mypy: bool = False,
        tests: bool = False,
    ) -> None:
        project = self.projects[project_name]

        # subprocess.check_call([
        #     sys.argv[0],
        #     # sys.executable,
        #     '-p', options.project,
        #     Command.meson_setup.value,
        # ])

        if argv is None:
            argv = []

        # assert argv is None or len(argv) == 0

        if not project.meson_path is None:
            if tests:
                self.meson_test(
                    project_name=project_name,
                )

            self.meson_install(
                project_name=project_name,
                force=force,
            )

        if mypy:
            self.mypy([])

        if env is None:
            env = dict()

        extra_args: list[str] = []

        if len(self.third_party_roots) > 0:
            extra_args.extend([
                '-Csetup-args=%s' % (
                    '-Dthird_party_roots=%s' % str(o.absolute())
                )
                for o in self.third_party_roots
            ])

        cmd = [
            sys.executable,
            '-m',
            'build',
            '-w', '-n',
            *extra_args,
            '-Csetup-args=-Dmodes=pyproject',
            '-Cbuild-dir=%s' % str(project.build_dir / 'pyproject'),
            '-Csetup-args=-Dinstall_path=%s' % str(project.dest_dir),
            # '-Cbuild-dir=%s' % str(project.build_dir),
            str(project.source_dir),
            *argv,
        ]

        if not output_dir is None:
            cmd.extend(['-o', str(output_dir)])

        logger.info(dict(env=env))

        subprocess.check_call(
            cmd,
            env=dict(list(os.environ.items())) | env,
        )

        if not project.meson_path is None:
            if tests:
                subprocess.check_call(
                    [
                        'ninja',
                        '-C',
                        str(project.build_dir / 'pyproject'),
                        'test',
                    ]
                )

    def meson_install(
        self,
        project_name: str,
        force: Optional[bool] = None,
        argv: Optional[list[str]] = None,
    ) -> None:
        project = self.projects[project_name]

        if force is None:
            force = False

        if argv is None:
            argv = []

        if force and project.dest_dir.exists():
            shutil.rmtree(project.dest_dir)

        subprocess.check_call([
            shutil_which('meson', True,),
            'install',
            '-C',
            project.build_dir / 'meson',
            '--destdir', project.dest_dir,
            *argv,
        ])

        for o in glob.glob(
            str(project.dest_dir / 'lib' / 'pkgconfig' / '*.pc'),
            recursive=True,
        ):
            logger.info(dict(
                path=o,
                action='patch prefix',
            ))

            with io.open(o, 'r') as f:
                content = f.read()

            with io.open(o, 'w') as f:
                f.write(
                    content.replace('prefix=/', 'prefix=${pcfiledir}/../../')
                )
    def ninja(
        self,
        project_name: str,
        argv: Optional[list[str]] = None,
        env: Optional[dict[str, str]] = None,
    ) -> None:
        project = self.projects[project_name]

        if argv is None:
            argv = []

        if env is None:
            env = dict()

        logger.info(dict(env=env))

        subprocess.check_call(
            [
                shutil_which('ninja', True),
                '-C',
                str(project.build_dir / 'meson'),
                *argv,
            ],
            env=dict(list(os.environ.items())) | env,
        )

    def meson_test(
        self,
        project_name: str,
        argv: Optional[list[str]] = None,
    ) -> None:
        project = self.projects[project_name]

        if argv is None:
            argv = []

        subprocess.check_call([
            shutil_which('meson', True,),
            'test',
            '-C',
            project.build_dir / 'meson',
            *argv,
        ])


    def meson_compile(
        self,
        project_name: str,
        argv: Optional[list[str]] = None,
    ) -> None:
        project = self.projects[project_name]

        if argv is None:
            argv = []

        subprocess.check_call([
            shutil_which('meson', True,),
            'compile',
            '-C',
            project.build_dir / 'meson',
            *argv,
        ])

    @property
    def third_party_roots(self) -> list[pathlib.Path]:
        return []

    def meson_setup(
        self,
        project_name: str,
        force: bool,
        argv: Optional[list[str]] = None,
        env: Optional[dict[str, str]] = None,
        # third_party_roots: Optional[list[pathlib.Path]] = None,
    ) -> None:
        project = self.projects[project_name]

        if argv is None:
            argv = []

        if env is None:
            env = dict()

        logger.info(dict(env=env))

        if force:
            if (project.build_dir / 'meson').exists():
                logger.info(dict(action='removing build dir', path=project.build_dir / 'meson'))
                shutil.rmtree(project.build_dir / 'meson')

        extra_args : list[str] = []

        if len(self.third_party_roots) > 0:
            extra_args.extend([
                '-Dthird_party_roots=%s' % str(o.absolute())
                for o in self.third_party_roots
            ])

        cmd = [
            shutil_which('meson', True,),
            'setup',
            str(project.source_dir),
            str(project.build_dir / 'meson'),
            '-Dmodes=["meson"]',
            *extra_args,
            # '-Dpkgconfig.relocatable=true',
            '-Dprefix=/',
            *argv,
        ]

        logger.info(dict(cmd=cmd))

        subprocess.check_call(
            cmd,
            env=dict(list(os.environ.items())) | env,
        )
