"""AI Provider abstraction layer.

Unified interface for code generation across multiple AI backends:
  - OpenAI (GPT-4o, etc.)
  - Cursor CLI (repo-aware edits, the core builder workflow)
  - Claude Code (Anthropic's agentic coding CLI)
  - OpenAI Codex CLI (repo-aware agentic coding)

Switchable via AI_PROVIDER in .env. Each provider implements the same Protocol
so the codegen domain doesn't care which one is active.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Protocol, runtime_checkable

from src.config import AIProvider, get_settings


@dataclass
class CodegenResult:
    """Standardized result from any AI provider."""
    commit_sha: Optional[str] = None
    pr_url: Optional[str] = None
    summary: str = ""
    plan: Optional[dict] = None
    files_changed: Optional[list[str]] = None
    files_changed_detail: Optional[list[dict]] = None
    tokens_used: int = 0
    provider: str = ""
    # True when the repo-based CLI exited non-zero, timed out, or was missing — run must end as ``failed``, not ``completed``.
    provider_invocation_failed: bool = False
    # Parsed ``p2hs/required-user-inputs.json`` when the agent requested owner-supplied public config (URLs, IDs).
    pending_user_inputs: Optional[dict] = None
    # Parsed ``p2hs/functions.json`` — library functions the agent wants deployed for the project.
    pending_fn_deploys: Optional[list] = None
    # Parsed ``p2hs/feature-packs.json`` — feature packs the agent wants installed for the project.
    pending_pack_installs: Optional[list] = None
    # Parsed ``p2hs/feature-packs.json``'s "compose" array — Level 1 capability composition
    # requirements (see src/domain/feature_packs/requirements_spec.py) for a requirement no
    # catalog pack already covers.
    pending_pack_compositions: Optional[list] = None
    # Outcomes of actually resolving pending_pack_installs/pending_pack_compositions --
    # populated inside _run_repo_based_codegen, BEFORE the push (same "pre-push gate"
    # pattern as provision_codegen_resources), so a required primitive_gap can veto the
    # push instead of only being discovered after the code already shipped. run_codegen
    # reads these instead of re-invoking _process_pack_installs/_process_pack_compositions.
    pack_install_outcomes: Optional[list] = None
    composition_outcomes: Optional[list] = None
    # Parsed ``p2hs/provision.json`` — platform resources (storage buckets) the agent wants provisioned.
    pending_provision: Optional[dict] = None
    # Parsed ``p2hs/email-templates.json`` — email template copy overrides the agent wants applied.
    pending_email_templates: Optional[dict] = None
    # Core-owned project-scoped L3 declarations materialized before provider execution.
    pending_l3_candidates: Optional[list[dict]] = None
    # Cursor Agent CLI chat/session id parsed from stream-json (persist per project for ``--resume``).
    cursor_session_id: Optional[str] = None


@runtime_checkable
class AICodeGeneratorProtocol(Protocol):
    """Protocol all AI code gen providers must satisfy."""

    provider_name: str

    async def generate_plan(
        self,
        system_prompt: str,
        user_prompt: str,
        repo_path: Optional[str] = None,
        line_callback: Optional[object] = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """Generate a markdown implementation plan (read-only, no code changes).

        repo_path: optional path to cloned repo for providers that need repo context.
        line_callback: optional async callable(text, stream="stdout", *, detail_format=None);
            detail_format is "stream-json" for Cursor NDJSON progress lines when enabled.
            Older single- and two-arg callables still work.
        project_meta_path: optional P2HS meta dir (rules written here, not only inside the checkout).
        """
        ...

    async def apply_changes(
        self,
        repo_path: str,
        plan: dict,
        system_prompt: str,
        line_callback: Optional[object] = None,
        *,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """Apply code changes to a repo checkout. Returns commit info."""
        ...

    async def generate_and_apply(
        self,
        repo_path: str,
        system_prompt: str,
        user_prompt: str,
        line_callback: Optional[object] = None,
        *,
        cursor_resume_session_id: Optional[str] = None,
        cursor_agent_home: Optional[str] = None,
        project_meta_path: Optional[str] = None,
    ) -> CodegenResult:
        """One-shot: plan + apply in a single call (for CLI-based providers).

        Cursor-only: ``cursor_resume_session_id`` / ``cursor_agent_home`` continue the same CLI chat;
        other providers ignore these keyword arguments.
        ``project_meta_path``: P2HS-only directory for rule files and Cursor agent state.
        """
        ...


def get_ai_provider(provider_override: str | AIProvider | None = None, *, api_key_override: str | None = None, model_override: str | None = None) -> "AICodeGeneratorProtocol":
    """Factory: return the configured provider, with an optional per-run override."""
    settings = get_settings()
    configured = provider_override or settings.ai_provider
    provider = AIProvider(configured)

    if provider == AIProvider.CURSOR:
        from src.integrations.cursor_cli import CursorCLIProvider
        return CursorCLIProvider(api_key=api_key_override, model=model_override)

    if provider == AIProvider.CLAUDE:
        from src.integrations.claude_code import ClaudeCodeProvider
        return ClaudeCodeProvider(api_key=api_key_override, model=model_override)

    if provider == AIProvider.CODEX:
        from src.integrations.codex_cli import CodexCLIProvider
        return CodexCLIProvider(api_key=api_key_override, model=model_override)

    from src.integrations.openai_client import OpenAIProvider
    return OpenAIProvider(api_key=api_key_override, model=model_override)
