from __future__ import annotations

import logging
import os
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Dict, Iterable, List, Optional, Sequence, Tuple

from models.git import get_repo_uuid_from_repo
from models.work_items import WorkItem, WorkItemStatusTransition
from providers.identity import IdentityResolver
from providers.status_mapping import StatusMapping

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class DiscoveredRepo:
    repo_id: uuid.UUID
    full_name: str
    source: str  # github|gitlab|local|...
    settings: Dict[str, object]


def _to_utc(dt: datetime) -> datetime:
    if dt.tzinfo is None:
        return dt.replace(tzinfo=timezone.utc)
    return dt.astimezone(timezone.utc)


def fetch_synthetic_work_items(
    *,
    repos: Sequence[DiscoveredRepo],
    days: int = 30,
) -> Tuple[List[WorkItem], List[WorkItemStatusTransition]]:
    """
    Generate synthetic work items for testing/demo purposes.
    """
    from fixtures.generator import SyntheticDataGenerator

    all_items: List[WorkItem] = []
    all_transitions: List[WorkItemStatusTransition] = []

    for repo in repos:
        if repo.source != "synthetic":
            continue
        logger.info("Generating synthetic work items for repo: %s", repo.full_name)
        # Use repo_id as random seed for stability
        import random

        seed = int(repo.repo_id.hex, 16) % (2**32)
        random.seed(seed)

        generator = SyntheticDataGenerator(
            repo_id=repo.repo_id, repo_name=repo.full_name
        )
        items = generator.generate_work_items(days=days)
        transitions = generator.generate_work_item_transitions(items)

        all_items.extend(items)
        all_transitions.extend(transitions)

    return all_items, all_transitions


def fetch_jira_work_items(
    *,
    since: datetime,
    until: Optional[datetime] = None,
    status_mapping: StatusMapping,
    identity: IdentityResolver,
    project_keys: Optional[Sequence[str]] = None,
) -> Tuple[List[WorkItem], List[WorkItemStatusTransition]]:
    """
    Fetch Jira issues updated since `since` and normalize into WorkItems.

    Jira configuration is provided via env vars:
    - JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN
    - optional: JIRA_PROJECT_KEYS (comma-separated)
    """
    try:
        from providers.jira.client import JiraClient, build_jira_jql
        from providers.jira.normalize import jira_issue_to_work_item
    except Exception as exc:  # pragma: no cover
        raise RuntimeError(f"Jira provider not available: {exc}") from exc

    if project_keys is None:
        raw_keys = os.getenv("JIRA_PROJECT_KEYS") or ""
        project_keys = [k.strip() for k in raw_keys.split(",") if k.strip()] or None

    jql_override = (os.getenv("JIRA_JQL") or "").strip()
    fetch_all = (os.getenv("JIRA_FETCH_ALL") or "").strip().lower() in {
        "1",
        "true",
        "yes",
        "on",
    }

    client = JiraClient.from_env()
    work_items: List[WorkItem] = []
    transitions: List[WorkItemStatusTransition] = []

    updated_since = _to_utc(since).date().isoformat()
    active_until = _to_utc(until).date().isoformat() if until is not None else None
    logger.info("Jira: fetching work items updated since %s", updated_since)
    jqls: List[str] = []
    if jql_override:
        jqls = [jql_override]
        logger.info("Jira: using JIRA_JQL override")
    elif fetch_all:
        logger.info("Jira: using JIRA_FETCH_ALL=1 (may be slow on large instances)")
        if project_keys:
            for key in project_keys:
                jqls.append(
                    build_jira_jql(
                        project_key=key, updated_since=None, active_until=None
                    )
                )
        else:
            jqls.append(
                build_jira_jql(project_key=None, updated_since=None, active_until=None)
            )
    else:
        if project_keys:
            for key in project_keys:
                jqls.append(
                    build_jira_jql(
                        project_key=key,
                        updated_since=updated_since,
                        active_until=active_until,
                    )
                )
        else:
            jqls.append(
                build_jira_jql(
                    project_key=None,
                    updated_since=updated_since,
                    active_until=active_until,
                )
            )

    for jql in jqls:
        logger.debug("Jira: JQL=%s", jql)
        for issue in client.iter_issues(jql=jql, expand_changelog=True):
            wi, wi_transitions = jira_issue_to_work_item(
                issue=issue,
                status_mapping=status_mapping,
                identity=identity,
                repo_id=None,
            )
            work_items.append(wi)
            transitions.extend(wi_transitions)

    logger.info("Fetched %d Jira work items (since %s)", len(work_items), updated_since)
    try:
        client.close()
    except Exception as exc:
        logger.warning("Failed to close Jira client: %s", exc)
    return work_items, transitions


def fetch_github_work_items(
    *,
    repos: Sequence[DiscoveredRepo],
    since: datetime,
    status_mapping: StatusMapping,
    identity: IdentityResolver,
    include_issue_events: bool = True,
    max_events_per_issue: int = 300,
) -> Tuple[List[WorkItem], List[WorkItemStatusTransition]]:
    """
    Fetch GitHub issues updated since `since` for the given repos and normalize into WorkItems.

    Requires `GITHUB_TOKEN`.
    """
    token = os.getenv("GITHUB_TOKEN") or ""
    if not token:
        raise ValueError("GitHub token required (set GITHUB_TOKEN)")

    from providers.github.client import GitHubAuth, GitHubWorkClient
    from providers.github.normalize import github_issue_to_work_item

    client = GitHubWorkClient(auth=GitHubAuth(token=token))
    work_items: Dict[str, WorkItem] = {}
    transitions: List[WorkItemStatusTransition] = []

    since_utc = _to_utc(since)
    github_repos = [r for r in repos if r.source == "github"]
    logger.info(
        "GitHub: fetching work items from %d repos (since %s)",
        len(github_repos),
        since_utc.isoformat(),
    )
    for repo in repos:
        if repo.source != "github":
            continue
        logger.debug("GitHub: repo=%s", repo.full_name)
        try:
            owner, name = repo.full_name.split("/", 1)
        except ValueError:
            continue
        for issue in client.iter_issues(
            owner=owner, repo=name, state="all", since=since_utc
        ):
            events = None
            if include_issue_events:
                try:
                    events = list(
                        client.iter_issue_events(issue, limit=max_events_per_issue)
                    )
                except Exception:
                    events = None
            wi, _transitions = github_issue_to_work_item(
                issue=issue,
                repo_full_name=repo.full_name,
                repo_id=repo.repo_id,
                status_mapping=status_mapping,
                identity=identity,
                events=events,
            )
            work_items[wi.work_item_id] = wi
            transitions.extend(list(_transitions or []))

    logger.info(
        "Fetched %d GitHub work items (since %s)",
        len(work_items),
        since_utc.isoformat(),
    )
    return list(work_items.values()), transitions


def fetch_github_project_v2_items(
    *,
    projects: Sequence[Tuple[str, int]],
    status_mapping: StatusMapping,
    identity: IdentityResolver,
) -> Tuple[List[WorkItem], List[WorkItemStatusTransition]]:
    """
    Fetch GitHub Projects v2 items for (org_login, project_number).

    Configure via env `GITHUB_PROJECTS_V2` as comma-separated `org:project_number` entries:
      GITHUB_PROJECTS_V2="myorg:3,anotherorg:12"
    """
    token = os.getenv("GITHUB_TOKEN") or ""
    if not token:
        raise ValueError("GitHub token required (set GITHUB_TOKEN)")

    from providers.github.client import GitHubAuth, GitHubWorkClient
    from providers.github.normalize import github_project_v2_item_to_work_item

    client = GitHubWorkClient(auth=GitHubAuth(token=token))
    items: Dict[str, WorkItem] = {}
    transitions: List[WorkItemStatusTransition] = []
    for org_login, project_number in projects:
        project_scope_id = f"ghprojv2:{org_login}#{int(project_number)}"
        logger.info("GitHub: fetching Projects v2 items for %s", project_scope_id)
        for node in client.iter_project_v2_items(
            org_login=org_login, project_number=int(project_number), first=50
        ):
            wi = github_project_v2_item_to_work_item(
                item_node=node,
                project_scope_id=project_scope_id,
                status_mapping=status_mapping,
                identity=identity,
            )
            if wi is None:
                continue
            items[wi.work_item_id] = wi

    logger.info("Fetched %d GitHub Projects v2 items", len(items))
    return list(items.values()), transitions


def parse_github_projects_v2_env() -> List[Tuple[str, int]]:
    raw = os.getenv("GITHUB_PROJECTS_V2") or ""
    projects: List[Tuple[str, int]] = []
    for part in raw.split(","):
        part = part.strip()
        if not part:
            continue
        try:
            org, number_str = part.split(":", 1)
            projects.append((org.strip(), int(number_str.strip())))
        except Exception:
            continue
    return projects


def fetch_gitlab_work_items(
    *,
    repos: Sequence[DiscoveredRepo],
    since: datetime,
    status_mapping: StatusMapping,
    identity: IdentityResolver,
    include_label_events: bool = True,
    max_label_events: int = 300,
) -> Tuple[List[WorkItem], List[WorkItemStatusTransition]]:
    """
    Fetch GitLab issues updated since `since` for the given projects and normalize into WorkItems.

    Requires `GITLAB_TOKEN` and optional `GITLAB_URL`.
    """
    from providers.gitlab.client import GitLabWorkClient
    from providers.gitlab.normalize import gitlab_issue_to_work_item

    client = GitLabWorkClient.from_env()
    work_items: Dict[str, WorkItem] = {}
    transitions: List[WorkItemStatusTransition] = []

    since_utc = _to_utc(since)
    gitlab_repos = [r for r in repos if r.source == "gitlab"]
    logger.info(
        "GitLab: fetching work items from %d projects (since %s)",
        len(gitlab_repos),
        since_utc.isoformat(),
    )
    for repo in repos:
        if repo.source != "gitlab":
            continue
        logger.debug("GitLab: project=%s", repo.full_name)
        for issue in client.iter_project_issues(
            project_id_or_path=repo.full_name,
            state="all",
            updated_after=since_utc,
        ):
            label_events = None
            if include_label_events:
                try:
                    # python-gitlab provides resource_label_events on issue objects.
                    label_events = list(
                        issue.resource_label_events.list(per_page=100, iterator=True)
                    )[:max_label_events]
                except Exception:
                    label_events = None

            wi, _transitions = gitlab_issue_to_work_item(
                issue=issue,
                project_full_path=repo.full_name,
                repo_id=repo.repo_id,
                status_mapping=status_mapping,
                identity=identity,
                label_events=label_events,
            )
            work_items[wi.work_item_id] = wi
            transitions.extend(list(_transitions or []))

    logger.info(
        "Fetched %d GitLab work items (since %s)",
        len(work_items),
        since_utc.isoformat(),
    )
    return list(work_items.values()), transitions


def discover_repos_from_records(
    records: Iterable[Tuple[str, Dict[str, object]]],
) -> List[DiscoveredRepo]:
    """
    Convert (repo_full_name, settings) tuples into DiscoveredRepo rows.
    """
    repos: List[DiscoveredRepo] = []
    for full_name, settings in records:
        source = str((settings or {}).get("source") or "").strip().lower()
        if not source:
            continue
        try:
            repo_id = get_repo_uuid_from_repo(full_name)
        except Exception:
            continue
        repos.append(
            DiscoveredRepo(
                repo_id=repo_id,
                full_name=full_name,
                source=source,
                settings=settings or {},
            )
        )
    return repos
