import os
import warnings
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

import tomli


class ConfigError(Exception):
    pass


@dataclass
class OutputConfig:
    dir: str = "output"
    language: str = "python"
    framework: str | None = None


@dataclass
class TestConfig:
    runner: str = "pytest"
    coverage: bool = True
    coverage_threshold: float = 80.0


@dataclass
class AuditConfig:
    max_fix_cycles: int = 3


@dataclass
class BuildConfig:
    max_fix_attempts: int = 3
    setup: str | None = None
    verify: str | None = None
    test: str | None = None


DEFAULT_MODEL = "anthropic:claude-sonnet-4-6"
DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434/v1"

TOOL_REQUIRED_ROLES = frozenset({"build", "fixer"})


@dataclass
class LLMConfig:
    model: str = DEFAULT_MODEL
    audit: str | None = None
    build: str | None = None
    planner: str | None = None
    brief: str | None = None
    interface: str | None = None
    fixer: str | None = None
    ollama_base_url: str = DEFAULT_OLLAMA_BASE_URL
    retries: int = 3
    tool_retries: int = 5

    @property
    def uses_ollama(self) -> bool:
        models = [
            self.model,
            self.audit,
            self.build,
            self.planner,
            self.brief,
            self.interface,
            self.fixer,
        ]
        return any(m is not None and m.startswith("ollama:") for m in models)

    def model_for(self, role: str) -> str:
        override: str | None = getattr(self, role, None)
        if override is not None:
            return override
        return self.model


@dataclass
class OssatureConfig:
    name: str = "ossature-project"
    version: str = "0.0.1"
    spec_dir: str = "specs"
    context_dir: str = "context"

    output: OutputConfig = field(default_factory=OutputConfig)
    test: TestConfig = field(default_factory=TestConfig)
    audit: AuditConfig = field(default_factory=AuditConfig)
    build: BuildConfig = field(default_factory=BuildConfig)
    llm: LLMConfig = field(default_factory=LLMConfig)

    root: Path = field(default_factory=Path.cwd)

    @property
    def spec_path(self) -> Path:
        return self.root / self.spec_dir

    @property
    def context_path(self) -> Path:
        return self.root / self.context_dir

    @property
    def output_path(self) -> Path:
        return self.root / self.output.dir

    @property
    def metadata_path(self) -> Path:
        return self.root / ".ossature"

    @property
    def metadata_context_path(self) -> Path:
        return self.metadata_path / "context"

    @property
    def metadata_context_spec_briefs_path(self) -> Path:
        return self.metadata_context_path / "spec-briefs"

    @property
    def metadata_context_interfaces_path(self) -> Path:
        return self.metadata_context_path / "interfaces"

    @property
    def is_audited(self) -> bool:
        return self.metadata_path.exists()


def find_config(start_path: Path | None = None) -> Path | None:
    current = start_path or Path.cwd()
    current = current.resolve()

    while current != current.parent:
        config_path = current / "ossature.toml"
        if config_path.exists():
            return config_path
        current = current.parent

    config_path = current / "ossature.toml"
    if config_path.exists():
        return config_path

    return None


def _parse_output_config(data: dict[str, Any]) -> OutputConfig:
    return OutputConfig(
        dir=data.get("dir", "."),
        language=data.get("language", "python"),
    )


def _parse_test_config(data: dict[str, Any]) -> TestConfig:
    return TestConfig(
        runner=data.get("runner", "pytest"),
        coverage=data.get("coverage", True),
        coverage_threshold=float(data.get("coverage_threshold", 80.0)),
    )


def _parse_audit_config(data: dict[str, Any]) -> AuditConfig:
    return AuditConfig(
        max_fix_cycles=int(data.get("max_fix_cycles", 3)),
    )


def _parse_build_config(data: dict[str, Any]) -> BuildConfig:
    return BuildConfig(
        max_fix_attempts=int(data.get("max_fix_attempts", 3)),
        setup=data.get("setup"),
        verify=data.get("verify"),
        test=data.get("test"),
    )


def _parse_llm_config(data: dict[str, Any]) -> LLMConfig:
    return LLMConfig(
        model=data.get("model", DEFAULT_MODEL),
        audit=data.get("audit"),
        build=data.get("build"),
        planner=data.get("planner"),
        brief=data.get("brief"),
        interface=data.get("interface"),
        fixer=data.get("fixer"),
        ollama_base_url=data.get("ollama_base_url", DEFAULT_OLLAMA_BASE_URL),
        retries=int(data.get("retries", 3)),
        tool_retries=int(data.get("tool_retries", 5)),
    )


def load_config(path: Path | None = None) -> OssatureConfig:
    if path is None:
        path = find_config()
        if path is None:
            raise ConfigError("No ossature.toml found.")

    if not path.exists():
        raise ConfigError(f"Config file not found: {path}")

    try:
        content = path.read_text(encoding="utf-8")
        data = tomli.loads(content)
    except Exception as e:
        raise ConfigError(f"Failed to parse {path}: {e}")

    project = data.get("project", {})

    llm_data = data.get("llm")
    if llm_data is None:
        raise ConfigError(
            "Missing [llm] section in ossature.toml. Add at minimum:\n\n"
            "  [llm]\n"
            '  model = "anthropic:claude-sonnet-4-6"'
        )
    if "model" not in llm_data:
        raise ConfigError(
            "Missing 'model' in [llm] section. Set the default model, e.g.:\n\n"
            "  [llm]\n"
            '  model = "anthropic:claude-sonnet-4-6"'
        )

    config = OssatureConfig(
        name=project.get("name", "ossature-project"),
        version=project.get("version", "0.0.1"),
        spec_dir=project.get("spec_dir", "specs"),
        context_dir=project.get("context_dir", "context"),
        output=_parse_output_config(data.get("output", {})),
        test=_parse_test_config(data.get("test", {})),
        audit=_parse_audit_config(data.get("audit", {})),
        build=_parse_build_config(data.get("build", {})),
        llm=_parse_llm_config(llm_data),
        root=path.parent,
    )

    if config.llm.uses_ollama and "OLLAMA_BASE_URL" not in os.environ:
        os.environ["OLLAMA_BASE_URL"] = config.llm.ollama_base_url

    _warn_redundant_cd(config)

    return config


def _warn_redundant_cd(config: OssatureConfig) -> None:
    output_dir = config.output.dir
    prefix = f"cd {output_dir}"
    fields = {"setup": config.build.setup, "verify": config.build.verify, "test": config.build.test}
    for field_name, command in fields.items():
        if not command:
            continue
        stripped = command.lstrip()
        if not stripped.startswith(prefix):
            continue
        rest = stripped[len(prefix) :]
        if rest == "" or rest[0] in (" ", "\t", ";", "&"):
            warnings.warn(
                f"[build] {field_name} contains 'cd {output_dir}' — "
                f"this is unnecessary. All build commands already run "
                f"inside the output directory ({output_dir!r}).",
                stacklevel=2,
            )
