"""Sync engine — forward sync (spec → tickets) and reverse sync (tickets → spec).

Supports configurable ticket mapping via TicketSystemConfig: custom templates,
field mapping, hierarchy (issue types + auto-parenting), and status maps.
All config is optional — when omitted, behavior matches the original hardcoded engine.
"""

from __future__ import annotations

import logging

from specwright.parser.models import SpecDocument, SpecSection
from specwright.parser.writer import (
    StatusUpdate,
    TicketLinkInsertion,
    insert_ticket_links,
    update_status_comments,
)
from specwright.sync.adapters.base import TicketAdapter
from specwright.sync.field_resolver import resolve_fields
from specwright.sync.hierarchy import find_parent_ticket_id, resolve_issue_type
from specwright.sync.mapping import TicketSystemConfig
from specwright.sync.models import (
    CreateTicketInput,
    SyncCreated,
    SyncError,
    SyncResult,
    SyncSkipped,
    SyncStatusChanged,
    SyncUpdated,
)
from specwright.sync.status_map import resolve_reverse
from specwright.sync.templates import render_description, render_summary

logger = logging.getLogger(__name__)

# Only sections in these states are actionable work items.
_SYNCABLE_STATES = frozenset({"todo", "in_progress"})


def _flatten_sections(sections: list[SpecSection]) -> list[SpecSection]:
    """Recursively flatten a section tree into a pre-order list."""
    result: list[SpecSection] = []
    for section in sections:
        result.append(section)
        if section.children:
            result.extend(_flatten_sections(section.children))
    return result


def _detect_system(adapter: TicketAdapter, system_config: TicketSystemConfig | None) -> str:
    """Determine the ticket system name.

    Prefers the explicit ``system_config.system`` value when available, falling
    back to class-name heuristics for legacy callers that don't pass config.
    """
    if system_config and system_config.system:
        return system_config.system
    name = type(adapter).__name__
    if name == "JiraAdapter":
        return "jira"
    if name == "LinearAdapter":
        return "linear"
    if name == "GitHubAdapter":
        return "github"
    return "unknown"


async def forward_sync(
    doc: SpecDocument,
    adapter: TicketAdapter,
    project_key: str,
    *,
    require_review: bool = False,
    dry_run: bool = False,
    system_config: TicketSystemConfig | None = None,
) -> tuple[str, SyncResult]:
    """Forward sync: create tickets for sections without one.

    When ``require_review`` is True, sync is blocked unless the spec's
    frontmatter ``review_status`` is ``"approved"``.

    When ``system_config`` is provided, uses its templates, field mapping,
    hierarchy, and status map for ticket creation. When omitted, falls back
    to the original hardcoded behavior.

    Returns (updated_markdown, sync_result).
    """
    if require_review:
        review_status = getattr(doc.frontmatter, "review_status", None)
        if review_status != "approved":
            result = SyncResult()
            result.errors.append(
                SyncError(
                    section_id="__document__",
                    error=(
                        f"Spec requires review approval before ticket sync "
                        f"(current review_status: {review_status!r})"
                    ),
                )
            )
            return doc.raw, result

    result = SyncResult()
    insertions: list[TicketLinkInsertion] = []
    all_sections = _flatten_sections(doc.sections)
    spec_title = doc.frontmatter.title

    # Pre-extract config sub-objects (None-safe)
    templates_cfg = system_config.templates if system_config else None
    field_map_cfg = system_config.field_map if system_config else None
    hierarchy_cfg = system_config.hierarchy if system_config else None

    # Track processed sections for auto-parenting
    processed_sections: list[SpecSection] = []

    for section in all_sections:
        if not section.section_number:
            continue

        # Only create tickets for actionable sections (todo, in_progress).
        # Draft, done, blocked, deprecated sections are not work items.
        if section.status.state not in _SYNCABLE_STATES and not section.ticket_link:
            result.skipped.append(
                SyncSkipped(
                    section_id=section.id,
                    reason=f"status is {section.status.state!r} (only todo/in_progress are synced)",
                )
            )
            continue

        if not section.ticket_link:
            # Resolve issue type via hierarchy config
            issue_type = resolve_issue_type(section, hierarchy_cfg)

            # Resolve summary and description via templates or defaults
            if templates_cfg and (templates_cfg.summary or templates_cfg.description):
                summary = render_summary(section, doc, templates_cfg)
                description = render_description(section, doc, templates_cfg)
            else:
                # Include issue type in default summary when hierarchy is active
                if hierarchy_cfg and hierarchy_cfg.depth_to_type:
                    summary = (
                        f"[{issue_type}] [{spec_title} §{section.section_number}] {section.title}"
                    )
                else:
                    summary = f"[{spec_title} §{section.section_number}] {section.title}"
                description = section.content[:2000]

            # Resolve auto-parent ticket ID
            parent_ticket_id = find_parent_ticket_id(section, processed_sections, hierarchy_cfg)

            # Resolve field mappings
            standard_fields, custom_fields = resolve_fields(section, doc, field_map_cfg)

            # Build labels: field-mapped labels + issue type label
            labels: list[str] = []
            mapped_labels = standard_fields.get("labels")
            if isinstance(mapped_labels, (list, tuple)):
                labels.extend(str(lb) for lb in mapped_labels)
            if hierarchy_cfg and hierarchy_cfg.depth_to_type:
                labels.append(f"type:{issue_type.lower()}")

            if dry_run:
                result.created.append(
                    SyncCreated(
                        section_id=section.id,
                        ticket_id="(dry-run)",
                        ticket_url="",
                    )
                )
                processed_sections.append(section)
                continue
            try:
                ticket = await adapter.create_ticket(
                    CreateTicketInput(
                        project_key=project_key,
                        summary=summary,
                        description=description,
                        status=section.status,
                        issue_type=issue_type,
                        parent_ticket_id=parent_ticket_id,
                        custom_fields=custom_fields,
                        labels=labels,
                    )
                )
                insertions.append(
                    TicketLinkInsertion(
                        heading_line=section.start_line,
                        system=_detect_system(adapter, system_config),
                        ticket_id=ticket.ticket_id,
                    )
                )
                result.created.append(
                    SyncCreated(
                        section_id=section.id,
                        ticket_id=ticket.ticket_id,
                        ticket_url=ticket.ticket_url,
                    )
                )
            except Exception as err:
                result.errors.append(SyncError(section_id=section.id, error=str(err)))
        else:
            result.updated.append(
                SyncUpdated(
                    section_id=section.id,
                    ticket_id=section.ticket_link.ticket_id,
                )
            )

        # Track section even on error — it still occupies a position in the
        # hierarchy. Auto-parenting gates on ticket_link, so children of a
        # failed parent won't get a spurious parent ID.
        processed_sections.append(section)

    markdown = insert_ticket_links(doc, insertions) if insertions else doc.raw
    return markdown, result


async def reverse_sync(
    doc: SpecDocument,
    adapter: TicketAdapter,
    *,
    system_config: TicketSystemConfig | None = None,
) -> tuple[str, SyncResult]:
    """Reverse sync: poll ticket statuses and update spec status comments.

    When ``system_config`` is provided, uses its ``status_map`` to resolve
    ticket statuses to spec states. When omitted, uses default mappings.

    Returns (updated_markdown, sync_result).
    """
    result = SyncResult()
    updates: list[StatusUpdate] = []
    all_sections = _flatten_sections(doc.sections)

    system = _detect_system(adapter, system_config)
    status_map_cfg = system_config.status_map if system_config else None

    for section in all_sections:
        if not section.ticket_link or not section.section_number:
            continue

        try:
            ticket_status = await adapter.get_ticket_status(section.ticket_link.ticket_id)

            # Use configurable status resolution when available
            if status_map_cfg and status_map_cfg.reverse:
                resolved = resolve_reverse(system, ticket_status.raw_status, status_map_cfg)
                new_state = resolved.state
            else:
                new_state = ticket_status.status.state

            if new_state != section.status.state:
                updates.append(
                    StatusUpdate(
                        section_number=section.section_number,
                        new_state=new_state,
                    )
                )
                result.status_changed.append(
                    SyncStatusChanged(
                        section_id=section.id,
                        ticket_id=section.ticket_link.ticket_id,
                        old_state=section.status.state,
                        new_state=new_state,
                    )
                )
        except Exception as err:
            result.errors.append(SyncError(section_id=section.id, error=str(err)))

    markdown = update_status_comments(doc, updates) if updates else doc.raw
    return markdown, result
